COMP3141 Software System Design and Implementation

COMP3141: Software System Design and Implementation

Term 2, 2023

Quiz (Week 3)

Table of Contents

Hint: some of these questions can be a bit subtle, and will require you to think it through. Don't rush it, and don't try it without tool support. You should make full use of the GHCi Haskell interpreter, Hoogle, and QuickCheck, as appropriate.

1 Properties for Functions

1.1 Question 1

Select all the properties that list append (++) satisfies.

  1. xs ++ (ys ++ zs) == (xs ++ ys) ++ zs
  2. xs ++ ys == ys ++ xs
  3. xs ++ [] == xs
  4. [] ++ xs == xs
  5. xs ++ xs == xs
  6. xs ++ [] == []

These properties are exactly the monoid laws. Lists (for the theoretically inclined) form a free monoid over the element type, meaning among other things that it has no interesting algebraic laws beyond the monoid laws.

The second property holds in the special case where the element type has only one member, but not in general.

1.2 Question 2

A substitution cipher is a method of hiding a message by substituting each character in the message text with a different character in a way that can be reversed. The ROT13 Cipher is a simple substitution cipher which rotates the alphabet by thirteen places, e.g. sends A to N and Z to M. Here is a simple (rather inefficient) Haskell implementation:

rot13 :: String -> String
rot13 = map $ \x -> 
          case lookup x table of
            Just y  -> y 
            Nothing -> x
  where
    table = table' 'A' 'Z' ++ table' 'a' 'z'
    table' a z = zip [a..z] (drop 13 (cycle [a..z]))

Select all the properties that this function satisfies (where the variables range over ASCII strings).

  1. length x == length (rot13 x)
  2. rot13 (map toUpper x) == map toUpper (rot13 x)
  3. rot13 (map f x) == map f (rot13 x)
  4. all (not . isAlpha) x ==> rot13 x == x
  5. rot13 (a ++ b) == rot13 a ++ rot13 b
  6. not (null x) ==> ord (head x) + 13 == ord (head (rot13 x))
  7. rot13 (rot13 x) == x

Famously, the ROT13 cipher is an involution, that is, it is its own inverse. This makes property 7 true.

Property 6 is false, as the difference in ASCII codes can be very different from 13, for example, the space character is left unaltered by ROT13 and so the difference may be zero.

Property 5 is true, as concatenating two ciphered strings is the same as ciphering their concatenation.

Property 4 is true, as rot13 does not affect any characters except alphabetical ones.

Property 3 does not hold, for example when f is the succ function.

Property 2 holds as case is preserved across ciphering (capital letters are changed to other capital letters, and lowercase letters are changed to other lowercase letters).

Property 1 is also true, as the ciphertext is always the same length as the plaintext in a substitution cipher.

1.3 Question 3

Consider the following two Haskell function definitions.

h :: [Int] -> [Int]
h = take 1

twist :: [Int] -> [Int]
twist [] = []
twist (x:xs) = xs ++ [x]

Select all the equalities below that hold for all inputs xs, ys.

  1. h (xs ++ xs) = h xs
  2. h (twist ys) = h ys
  3. h (twist xs ++ xs) = h (twist xs)
  4. twist (xs ++ h xs) = twist xs ++ h xs
  5. twist (h xs ++ xs) = xs ++ h xs

Property 1 holds: if xs is empty, then h ([] ++ []) = h []` as desired. If on the other hand ~xs is non-empty, then `xs + xs` and `xs` have the same first element, so ~h (xs + xs) = h xs` as desired.

Property 2 fails: h (twist [1,2]) = h [2,1] = [2] while h [1,2] = [1].

Property 3 holds, by a similar argument to Property 1.

Property 4 holds: if xs is empty, then twist ([] ++ h []) = twist [] = twist [] ++ [] = twist [] ++ h []. And if xs has the form (y:ys), then we have that twist ((y:ys) ++ h (y:ys)) = twist ((y:ys) ++ [y]) = twist (ys ++ [y] ++ [y]), and also that twist (y:ys) ++ h (y:ys) = twist (y:ys) ++ [y] = twist ys ++ [y] ++ [y] as well.

Property 5 holds by a similar argument to Property 4.

1.4 Question 4

The following function removes adjacent duplicates from a list.

dedup :: (Eq a) => [a] -> [a]
dedup (x:y:xs) | x == y = dedup (y:xs)
               | otherwise = x : dedup (y:xs)
dedup xs = xs

Assume the presence of the following sorted predicate:

sorted :: (Ord a) => [a] -> Bool
sorted (x:y:xs) = x <= y && sorted (y:xs)
sorted xs = True

Select all properties that dedup satisfies.

  1. sorted xs ==> sorted (dedup xs)
  2. sorted xs ==> dedup xs == nub xs
  3. sorted xs ==> dedup (dedup xs) == dedup xs
  4. sorted xs && sorted ys ==> dedup xs ++ dedup ys == dedup (xs ++ ys)
  5. sorted xs ==> length (dedup xs) < length xs
  6. (x `elem` xs) == (x `elem` dedup xs)

All of these properties are true except 4, as can be seen when both xs and ys are just the singleton list [1].

Property 1 is true as removing adjacent duplicates from a sorted list does not ruin the sorted ordering.

Property 2 is true as for sorted lists, removing adjacent duplicates and removing all duplicates are identical.

Property 3 is true as removing adjacent duplicates shouldn't find any more adjacent duplicates the second time around.

Property 5 is false as a list that already has no duplicates will not get any shorter.

Property 6 is true as dedup will not remove the last of any given value in the list.

2 Functions for Properties

2.1 Question 5

Select the functions below that are involutions. (A function f is an involution if it is its own inverse; that is, if f . f = id).

  1. succ
    
  2. id
    
  3. reverse
    
  4. negate::Int -> Int
    
  5. not
    
  6. const () :: () -> ()
    

TODO

2.2 Question 6

Here are a set of properties that the function foo must satisfy:

foo :: [a] -> (a -> b) -> [b]
foo = undefined -- see below

prop_1 :: [Int] -> Bool
prop_1 xs = foo xs id == xs 

prop_2 :: [Int] -> (Int -> Int) -> (Int -> Int) -> Bool
prop_2 xs f g = foo (foo xs f) g == foo xs (g . f)

Choose an implementation for foo that satisfies the above properties, and type-checks:

  1. foo xs f = []
    
  2. foo xs f = xs
    
  3. foo [] f = []
    foo (x:xs) f = x : foo xs f
    
  4. foo [] f = []
    foo (x:xs) f = f x : foo xs f
    
  5. foo [] f = []
    foo (x:xs) f = foo xs f
    

These are the standard laws (functor laws) that map has to obey. And, indeed, the correct answer is a map implementation.

Note that it is actually impossible to write a terminating function that typechecks and obeys those properties without correctly implementing map. Try to write an incorrect, terminating map that is well-typed and obeys those laws! You will find it is impossible. Later on in the course we will discuss why this is so and how we can exploit it to write better programs.

2.3 Question 7

bar :: [Int] -> [Int]
bar = undefined

prop_1 :: [Int] -> Bool
prop_1 xs = bar (bar xs) == xs

prop_2 :: [Int] -> Bool
prop_2 xs = length xs == length (bar xs)

prop_3 :: [Int] -> (Int -> Int) -> Bool
prop_3 xs f = bar (map f xs) == map f (bar xs)

Choose all implementations for bar that satisfy the above properties, and type-check:

  1. bar []     = []
    bar (x:xs) =      bar (filter (<=x) xs)
               ++ x : bar (filter (> x) xs)
    
  2. bar xs = go xs []
      where go []     acc = acc
            go (x:xs) acc = go xs (x:acc)
    
  3. bar []     = []
    bar (x:xs) = xs ++ [x]
    
  4. bar = id
    
  5. bar xs = nub xs
    
  6. bar xs = replicate (length xs) (maximum xs)
    

The first property says the function has to be an involution, that is, applying it twice should have the same effect as not applying it at all. Property 2 says that the number of elements must remain the same. And property 3 says that bar must commute with map: This effectively means that we cannot act upon the contents of the list, as this would allow the function given to map to be crafted to break this property.

Thus, we must permute the elements of the given list without altering them or changing their quantity, and we must choose the output permutation without inspecting the input values. Thus, the fast reverse implementation in 2, and the identity function in 4 are all correct implementations.

The rotation function in 3 breaks idempotence property 1. The remove duplicates function in 5 breaks the length property in 2. And the replace-with-maximum function in 6 breaks the map-commutation property in 3, for example if f is the absolute value function abs and the list given is [2,-4]. The quicksort function breaks property 3, as the map function could change the relative ordering of the elements.

2.4 Question 8

baz :: [Integer] -> Integer
baz = undefined

prop_1 :: [Integer] -> [Integer] -> Bool
prop_1 xs ys = baz xs + baz ys == baz (xs ++ ys)

prop_2 :: [Integer] -> Bool
prop_2 xs = baz xs == baz (reverse xs) 

prop_3 :: Integer -> [Integer] -> Bool 
prop_3 x xs = baz (x:xs) - x == baz xs

Choose a law-abiding definition for baz, that type checks:

  1. baz = foldr (+) 0
    
  2. baz []     = 0
    baz (x:xs) = 1 + baz xs
    
  3. baz []     = 1
    baz (x:xs) = x + baz xs
    
  4. baz xs = 0
    

This has to be a sum function (option 1). The game is almost given away by the first property alone. However prop_1 by itself allows for a function that merely returns zero (option 4) or a function that returns the length of the list (option 2), however prop_3 rules these out for us. Option 3 includes an off-by-one error, as the additive identity is 0 not 1, and thus would break prop_1 for even empty lists.

The prop_2 property is not really needed here, and is included as a bit of a red herring.

Submission is already closed for this quiz. You can click here to check your submission (if any).

2023-08-13 Sun 12:52

Announcements RSS